/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or https://opensource.org/licenses/CDDL-1.0.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/errno.h>
#include <sys/int_limits.h>	/* SIZE_MAX */
#include <sys/zfs_vfsops.h>
#include <sys/zfs_onexit.h>
#include <sys/zvol.h>
#include <sys/modctl.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/sunldi.h>

#include <sys/zfs_ioctl_impl.h>

extern struct modlfs zfs_modlfs;
ldi_ident_t zfs_li = NULL;
dev_info_t *zfs_dip;

uint64_t
zfs_max_nvlist_src_size_os(void)
{
	if (zfs_max_nvlist_src_size != 0)
		return (zfs_max_nvlist_src_size);

	return (SIZE_MAX);
}

int
zfs_vfs_ref(zfsvfs_t **zfvp)
{
	if (*zfvp == NULL)
		return (SET_ERROR(ESRCH));
	VFS_HOLD((*zfvp)->z_vfs);

	return (0);
}

boolean_t
zfs_vfs_held(zfsvfs_t *zfsvfs)
{
	return (zfsvfs->z_vfs != NULL);
}

void
zfs_vfs_rele(zfsvfs_t *zfsvfs)
{
	VFS_RELE(zfsvfs->z_vfs);
}

static int
zfs_ctldev_init(dev_t *devp)
{
	minor_t minor;
	zfs_soft_state_t *zss;
	zfsdev_state_t *zs;

	ASSERT(MUTEX_HELD(&zfsdev_state_lock));
	ASSERT(getminor(*devp) == 0);

	minor = zfsdev_minor_alloc();
	if (minor == 0)
		return (SET_ERROR(ENXIO));

	if (ddi_soft_state_zalloc(zfsdev_state, minor) != DDI_SUCCESS)
		return (SET_ERROR(EAGAIN));

	*devp = makedevice(getemajor(*devp), minor);

	zss = ddi_get_soft_state(zfsdev_state, minor);
	zs = kmem_alloc(sizeof (zfsdev_state_t), KM_SLEEP);
	zfs_onexit_init((zfs_onexit_t **)&zs->zs_onexit);
	zfs_zevent_init((zfs_zevent_t **)&zs->zs_zevent);
	zss->zss_type = ZSST_CTLDEV;
	zss->zss_data = zs;

	return (0);
}

static void
zfs_ctldev_destroy(zfsdev_state_t *zs, minor_t minor)
{
	ASSERT(MUTEX_HELD(&zfsdev_state_lock));
	ASSERT(zs != NULL);

	if (zs->zs_onexit != NULL)
		zfs_onexit_destroy((zfs_onexit_t *)zs->zs_onexit);
	if (zs->zs_zevent != NULL)
		zfs_zevent_destroy((zfs_zevent_t *)zs->zs_zevent);
	/* FIXME: stay in list */
	kmem_free(zs, sizeof (zfsdev_state_t));
	ddi_soft_state_free(zfsdev_state, minor);
}

static int
zfsdev_open(dev_t *devp, int flag, int otyp, cred_t *cr)
{
	int error = 0;

	if (getminor(*devp) != 0)
		return (zvol_open(devp, flag, otyp, cr));

	/* This is the control device. Allocate a new minor if requested. */
	if (flag & FEXCL) {
		mutex_enter(&zfsdev_state_lock);
		error = zfs_ctldev_init(devp);
		mutex_exit(&zfsdev_state_lock);
	}

	return (error);
}

static int
zfsdev_close(dev_t dev, int flag, int otyp, cred_t *cr)
{
	zfsdev_state_t *zo;
	minor_t minor = getminor(dev);

	if (minor == 0)
		return (0);

	mutex_enter(&zfsdev_state_lock);
	zo = zfsdev_get_soft_state(minor, ZSST_CTLDEV);
	if (zo == NULL) {
		mutex_exit(&zfsdev_state_lock);
		return (zvol_close(dev, flag, otyp, cr));
	}
	zfs_ctldev_destroy(zo, minor);
	mutex_exit(&zfsdev_state_lock);

	return (0);
}

static int
zfsdev_ioctl(dev_t dev, int cmd, intptr_t arg, int flag, cred_t *cr, int *rvalp)
{
	zfs_cmd_t *zc;
	uint_t vecnum;
	int error, rc;
	minor_t minor = getminor(dev);

	if (minor != 0 &&
	    zfsdev_get_soft_state(minor, ZSST_CTLDEV) == NULL)
		return (zvol_ioctl(dev, cmd, arg, flag, cr, rvalp));

	vecnum = cmd - ZFS_IOC_FIRST;
	ASSERT3U(getmajor(dev), ==, ddi_driver_major(zfs_dip));

	zc = kmem_zalloc(sizeof (zfs_cmd_t), KM_SLEEP);

	error = ddi_copyin((void *)(uintptr_t)arg, zc, sizeof (zfs_cmd_t),
	    flag);
	if (error != 0) {
		error = SET_ERROR(EFAULT);
		goto out;
	}

	error = zfsdev_ioctl_common(vecnum, zc, flag, cr); /* - ???*/

	rc = ddi_copyout(zc, (void *)(uintptr_t)arg, sizeof (zfs_cmd_t), flag);
	if (error == 0 && rc != 0)
		error = SET_ERROR(EFAULT);
out:
	kmem_free(zc, sizeof (zfs_cmd_t));
	return (error);
}

static int
zfs_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
	if (cmd != DDI_ATTACH)
		return (DDI_FAILURE);

	if (ddi_create_minor_node(dip, "zfs", S_IFCHR, 0,
	    DDI_PSEUDO, 0) == DDI_FAILURE)
		return (DDI_FAILURE);

	zfs_dip = dip;

	ddi_report_dev(dip);

	return (DDI_SUCCESS);
}

static int
zfs_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
	if (zfs_busy() || zvol_busy())
		return (DDI_FAILURE);

	if (cmd != DDI_DETACH)
		return (DDI_FAILURE);

	zfs_dip = NULL;

	ddi_prop_remove_all(dip);
	ddi_remove_minor_node(dip, NULL);

	return (DDI_SUCCESS);
}

/*ARGSUSED*/
static int
zfs_info(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result)
{
	switch (infocmd) {
	case DDI_INFO_DEVT2DEVINFO:
		*result = zfs_dip;
		return (DDI_SUCCESS);

	case DDI_INFO_DEVT2INSTANCE:
		*result = (void *)0;
		return (DDI_SUCCESS);
	}

	return (DDI_FAILURE);
}

/*
 * OK, so this is a little weird.
 *
 * /dev/zfs is the control node, i.e. minor 0.
 * /dev/zvol/[r]dsk/pool/dataset are the zvols, minor > 0.
 *
 * /dev/zfs has basically nothing to do except serve up ioctls,
 * so most of the standard driver entry points are in zvol.c.
 */
static struct cb_ops zfs_cb_ops = {
	zfsdev_open,	/* open */
	zfsdev_close,	/* close */
	zvol_strategy,	/* strategy */
	nodev,		/* print */
	zvol_dump,	/* dump */
	zvol_read,	/* read */
	zvol_write,	/* write */
	zfsdev_ioctl,	/* ioctl */
	nodev,		/* devmap */
	nodev,		/* mmap */
	nodev,		/* segmap */
	nochpoll,	/* poll */
	ddi_prop_op,	/* prop_op */
	NULL,		/* streamtab */
	D_NEW | D_MP | D_64BIT,		/* Driver compatibility flag */
	CB_REV,		/* version */
	nodev,		/* async read */
	nodev,		/* async write */
};

static struct dev_ops zfs_dev_ops = {
	DEVO_REV,	/* version */
	0,		/* refcnt */
	zfs_info,	/* info */
	nulldev,	/* identify */
	nulldev,	/* probe */
	zfs_attach,	/* attach */
	zfs_detach,	/* detach */
	nodev,		/* reset */
	&zfs_cb_ops,	/* driver operations */
	NULL,		/* no bus operations */
	NULL,		/* power */
	ddi_quiesce_not_needed,	/* quiesce */
};

static struct modldrv zfs_modldrv = {
	&mod_driverops,
	"ZFS storage pool",
	&zfs_dev_ops
};

static struct modlinkage modlinkage = {
	MODREV_1,
	(void *)&zfs_modlfs,
	(void *)&zfs_modldrv,
	NULL
};

int zfsdev_attach(void)
{
	return (mod_install(&modlinkage));
}

int zfsdev_detach(void)
{
	int error;

	if (zfs_busy() || zvol_busy() || zio_injection_enabled)
		return (SET_ERROR(EBUSY));

	if ((error = mod_remove(&modlinkage)) != 0)
		return (error);

	return (0);
}

/* Update the VFS's cache of mountpoint properties */
void
zfs_ioctl_update_mount_cache(const char *dsname)
{
	(void) dsname;
}

void
zfs_ioctl_init_os(void)
{
}

int
_init(void)
{
	int error;

	error = zfs_kmod_init();
	if (error == 0) {
		error = ldi_ident_from_mod(&modlinkage, &zfs_li);
		ASSERT(error == 0);
	}

	return (error);
}

int
_fini(void)
{
	int error;

	error = zfs_kmod_fini();
	if (error == 0) {
		ldi_ident_release(zfs_li);
		zfs_li = NULL;
	}

	return (error);
}

int
_info(struct modinfo *modinfop)
{
	return (mod_info(&modlinkage, modinfop));
}
